Offset Contents --------------------------------------------- 00 Start code, or 0 if none 04 Initialisation code, or 0 if none 08 Finalisation code, or 0 if none 0C Service handler code, or 0 if none 10 Title string (see below) 14 Help string (see below) 18 Command table, or 0 if none 1C SWI base number, or anything else 20 SWI handler code, or anything else 24 SWI decoding table, or anything else 28 SWI decoding code, or anything elseThe last four words are somewhat strange in that they may or may not be present. If they aren't present then data or code may be in that location instead. A good rule of thumb is that if any of the four entries would be rubbish as the SWI data it is considered that the section is invalid.
*RMKill
, and you
see when you type *Modules
. Therefore, it should not include any
spaces and should be descriptive of the module.
The help string is displayed when you do *Help <module
name>
or *Help Modules
. It should include the version
number and date string for then module and should be tabbed in so that it
lines up with the other module names. Most people like to stick their name
after the date string so that they get some credit for the module. You should
try to stick to the same kind of style as the standard Acorn modules.
This is very important because it means that if (at some point) it becomes viable to blow a group of modules into a ROM it would be nice if they would work by not writing to themselves. This isn't, of course, the main reason, but it is the one I live in hope of....
Because of this, modules should claim workspace in RMA to use for all their calls in their initialisation code, and release it in their finalisation code. When the init code is called it is passed a single word piece of workspace which it may use to store private data. Usually this is where the workspace pointer is stored, but regardless of the value, it will be passed on to all the other routines in r12.
&TTXX??NN
.*Configure
command,
though I'm only going by the code that I've seen, not having the manuals....
Firstly, we need to decide what we are going to do with our nice module. Because I'm lazy and I don't want to do anything difficult (and the fact that I just happen to have such a program lying around), I've decided that I want to write a *Command which redirects all output to a file instead of the screen, sort of like redirection.
Next, we need to decide what assembler we want to use. Since it is available on all computers, I'm going to use the built-in BASIC assembler. I don't use this for preference because modules are not easy to write for a group of reasons. However, I'll say more about that later...
What I want to do is to implement a command called WCDivert
(Write Character dirverter) which can take one parameter (the filename) to
start a diversion, or none to close the diversion.
EQUD 0 ; Start offset (ie none) EQUD init ; Initialisation offset EQUD final ; Finalisation offset EQUD 0 ; Service request offset (ie none) EQUD title ; Title string offset EQUD help ; Help string offset EQUD commands ; Help and command keyword table offsetThe title and header strings are quite easy to do :
.title EQUS "WCDivert"+CHR$0 ; *Modules string .help EQUS "WCDivert"+CHR$9+"1.00 ("+MID$(TIME$,5,11)+") © Justin Fletcher" EQUB 0 ; *Help Modules string ALIGN ; I'm not sure what's coming next ;-)Notice that I have used a semi-colon in the smiley on the last line. This is important not to the module, but to BASIC, because it thinks that any colon on a line (even in a comment) is a new command.
.commands EQUS "WCCapture"+CHR$(0); Command name ALIGN EQUD wccapture ; Code to call EQUB &00 ; Flags - minimum number of params EQUB &00 ; - erm... not a clue... EQUB &01 ; - maximum number of params EQUB &00 ; - normal command (I think) EQUD wcsyntax ; Syntax pointer EQUD wchelp ; Help pointer EQUB 0:ALIGN ; Finish the command tableI've split up the flags word there so that it is easier to see what is going on, but usually I retain it in the double word format.
The syntax and help strings are simply zero-terminated strings, so they are quite easy to set up :
.wcsyntax EQUS "Syntax: *WCCapture [<filename>]" EQUB 0 .wchelp EQUS "*WCCapture is used to start (giving a filename), or end a capture " EQUS "session. Fun, init ? " EQUB 0 ALIGNNot much of any real interest yet, but you should be getting the idea that things are not as difficult as they look - I hope...
.init STMFD (sp)!,{r0-r3,link} ; Stack registers MOV r0,#6 ; OS_Module code for claim MOV r3,#4 ; How many bytes ? SWI "XOS_Module" ; Claim private workspace ADDVS sp,sp,#4 ; if an error occurred then return LDMVSFD (sp),{r1-r4,pc} ; return with the error pointer STR r2,[r12] ; store our WS in private word MOV r0,#0 ; zero byte to initialise STR r0,[r2] ; store in workspace LDMFD (sp)!,{r0-r3,pc} ; Return from call safelyNotice the use of the X version of the SWI call when we claim the workspace; this is because we should return errors to the OS in the standard way (r0 -> error block), and therefore we skip r0 when we restore the registers and return.
However, because we are going to need to release the block later, we might as well just call that code.
.final STMFD (sp)!,{r0,r2,r4,link} ; Stack registers LDR r4,[r12] ; get the file handle CMP r4,#0 ; is it open ? BLEQ doclose ; if so, close and restore output LDR r2,[r12] ; read workspace pointer from private word MOV r0,#7 ; OS_Module release block SWI "XOS_Module" ; release the workspace STRVS r0,[sp] ; if error, store block on stack LDMFD (sp)!,{r0,r2,r4,pc} ; Return from call
doclose
is the routine I'm going to use which will close the
file and release the Wrch (Write Character) vector. I've decided that I'm
going to pass the file handle to the doclose
routine in r4.
Notice the way in which I have returned the error to the OS if it exists. I'm not overly keen on storing values on the stack like this, but it doesn't half make the code look neat. Because r0 is the lowest register stacked we can store the new value over the old one and then when it returns, because the flags are not restored the V flag is still set.
.wccapture STMFD (sp)!,{r0-r5,link} ; Stack registers LDR r12,[r12] ; read workspace from private wordWe have to read the workspace pointer from out of the private word first, otherwise we'll end up writing to the private word itself.
CMP r1,#1 ; is this a start ? BNE closecapture ; if not, jump to close code LDR r4,[r12] ; read the current handle CMP r4,#0 ; are we already diverting output ? BNE wcexit ; if so, exit (no error)We know that this is an open request and that we aren't redirecting at the moment, so we need to open the file and claim the vector.
MOV r1,r0 ; r1 -> string MOV r0,#&8c ; code to open file with errors SWI "XOS_Find" ; open file BVS wcexit ; if errors exit gracefully STR r0,[r12] ; store the handle in workspace ;-) ADR r1,intercept ; the code to use MOV r0,#3 ; the Wrch vector MOV r2,r12 ; pass to routine in r12 SWI "OS_Claim" ; claim vectorWe can now safely exit, and if there is an error we should return it.
.wcexit STRVS r0,[sp] ; if error, store block on stack LDMFD (sp)!,{r0-r5,pc} ; Return from call... and we need some code to handle if we are closing the file instead ...
.closecapture LDR r4,[r12] ; read file handle CMP r4,#0 ; are we actually diverting output ? BEQ wcexit ; if not, exit (no error) BL doclose ; do the close and release B wcexit ; exit nicely
doclose
routine is quite simple really, and simply closes
the file and releases the vector. Remember that I decreed that r4 would be
the file handle when I wrote the finalisation routine
.doclose STMFD (sp)!,{r0-r2,link} ; Stack registers ADR r1,intercept ; the code to release MOV r0,#3 ; WRCH vector MOV r2,r12 ; r12 as passed to the routine SWI "OS_Release" ; claim the WRCH vector MOV r0,#0 ; code for closing files MOV r1,r4 ; the file handle we're closing SWI "XOS_Find" ; close the file MOV r1,#0 ; mark as closed STR r1,[r12] ; store in the workspace LDMFD (sp)!,{r0-r2,pc} ; Return from callNotice that I've marked the file as closed in the workspace - otherwise when we try to
*RMKill
the module it will attempt to close files
which aren't open and make a mess all over the floor <grin>.
.intercept STMFD (sp)!,{r0-r1,link} ; Stack registers LDR r1,[r12] ; read the file handle from WS SWI "XOS_BPut" ; put the byte LDMFD (sp)!,{r0-r1,link} ; restore registers ; intercept the vector LDMFD (sp)!,{pc}^ ; Return from call with flagsI don't want any output to the screen, so I've restored the link register back into the link register and pulled the old pc off the stack along with all the flags. Restoring the flags on this occassion is very important because you must retain the same standards as the original call if you intercept a vector. If you wanted you could change the code so it didn't intercept the vector, but instead just wrote to a file as well as to the screen; however there is already a command to do that -
*Spool
!
REM >MakeWCC DIM m% 1024 sp=13:link=14:pc=15 FORI=4TO6 STEP2 P%=0:O%=m% [OPTI
] NEXT I SYS "OS_File",10,"$.WCC",&FFA,,m%,O% PRINT "Module saved"And that's about it really... It's not too difficult - the most difficult bit was the code to intercept WrchV, and not the making it into a module.